/*
 * Copyright the original author or authors.
 * 
 * Licensed under the MOZILLA PUBLIC LICENSE, Version 1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.mozilla.org/MPL/MPL-1.1.html
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.kairos.utils
{
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Graphics;
	import flash.display.PixelSnapping;
	import flash.display.Shape;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.text.TextField;
	import flash.text.TextFormat;
	
	import com.bourre.error.NullPointerException;
	import com.bourre.error.UnsupportedOperationException;
	import com.bourre.log.PixlibStringifier;
	import com.bourre.structures.Range;
	import com.kairos.engine.RTAnimation;
	import com.kairos.engine.RTBeacon;
	import com.kairos.engine.RTEvent;
	import com.kairos.log.KairosDebug;
	import com.kairos.utils.monitor.MonitorField;	

	/**
	 * The <code>Monitor</code> tool is a performance profiler, it display
	 * statistic on RTBeacon runs, framerate and memory usage.
	 * 
	 * <p>Use the <code>MonitorFieldsList</code> utility class to add the fields you
	 * needs in the monitor.</p>
	 * 
	 * @author  Cédric Néhémie
	 * @see		RTAnimation
	 * @example	Adding a statistic monitor to your project and adding two fields in it.
	 * 
	 * <listing>var stats : Monitor = new Monitor();
	 * 
	 * stats.addField( MonitorFieldsList.STEP );
	 * stats.addField( MonitorFieldsList.AVERAGE_STEP );
	 * 
	 * addChild( stats );
	 * 
	 * stats.start();
	 * </listing>
	 *  
	 * You can also specify the size of the monitor's graph by passing it to
	 * the constructor.
	 * 
	 * <listing>var stats : Monitor = new Monitor( 150, 200 );
	 * 
	 * stats.addField( MonitorFieldsList.STEP );
	 * stats.addField( MonitorFieldsList.AVERAGE_STEP );
	 * 
	 * addChild( stats );
	 * 
	 * stats.start();
	 * </listing>
	 */
	public class Monitor extends Sprite implements RTAnimation
	{				
		/**
		 * Color of the background of the monitor.
		 */
		public static var COLOR_BACKGROUND : Number = 0x111111;
		
		/**
		 * Color of the alternate background of monitor's fields.
		 */
		public static var COLOR_BACKGROUND_ALTERNATE1 : Number = 0x111111;
		
		/**
		 * Color of the second alternate background of monitor's fields.
		 */
		public static var COLOR_BACKGROUND_ALTERNATE2 : Number = 0x222222;
		
		/**
		 * Color of the background of the monitor's graph.
		 */
		public static var COLOR_GRAPH_BACKGROUND : Number = 0x000000;
		
		/**
		 * Color of the borders of the monitor's graph.
		 */
		public static var COLOR_GRAPH_BORDER : Number = 0x999999;
		
		/**
		 * Color of the values of the fields.
		 * 
		 * <p>Labels are always displayed with the field color.</p>
		 */
		public static var COLOR_VALUE : Number = 0xcccccc;
		
		// Graphical formatting values 
		private static const __MARGIN : Number = 5;
		private static const __LINE_SPACING : Number = 0;
		
		// Runner content
		protected var _bIsRunning : Boolean;		
		protected var _aFields : Array;
		protected var _aLabels : Array;
		protected var _oBeacon : RTBeacon;
				
		// Graph entity
		private var __nBMPWidth : Number;
		private var __nBMPHeight : Number;
		private var __bmpGraph : BitmapData;
		private var __bmpEmpty : BitmapData;
		private var __bmpTemp : BitmapData;
		
		// UI Entity
		private var __mcBackground : Shape;
		private var __mcBorder : Shape;
		private var __oGraphHolder : Bitmap;
		
		/**
		 * Creates a new <code>Monitor</code> object with a graph of size
		 * <code>width * height</code>.
		 * 
		 * <p>At creation the monitor don't contains any field, use the <code>addField</code>
		 * to add monitoring field in the monitor. You could find many field in the 
		 * <code>MonitorFieldsList</code>.</p>
		 *  
		 * @param	width	<code>Number</code> of pixel of the graph's width.
		 * @param 	height	<code>Number</code> of pixel of the graph's height.
		 */
		public function Monitor( width : Number = 200, height : Number = 100)
		{
			if( isNaN( width ) || width <= 0 )
				width = 200;
			if( isNaN( height ) || height <= 0 )
				height = 100;
			
			__nBMPWidth = width;
			__nBMPHeight = height;

			_aFields = new Array();
			_aLabels = new Array();
			
			_oBeacon = RTBeacon.getInstance();

			_createUI();
		}
		
		/**
		 * Adds the passed-in <code>MonitorField</code> into the monitor.
		 * 
		 * <p>Returns <code>true</code> if the field have been added
		 * in the monitor, <code>false</code> otherwise.</p>
		 * 
		 * @param  	o	<code>MonitorField</code> to add into the monitor.	
		 * @return 	<code>true</code> if the field have been added into
		 * 			the monitor, <code>false</code> otherwise.
		 */
		public function addField ( o : MonitorField ) : Boolean
		{
			if( o == null )
			{
				KairosDebug.WARN( "Attempt to add a null field in " + this );
				return false;
			}
			else if( !containsField( o ) )
			{			
				_aFields.push( o );
				o.setDisplayHeight( __nBMPHeight );
				
				if( o.isLabel() )
				{
					var label : MonitorFieldLabel = new MonitorFieldLabel ( o );
					_aLabels.push( label );
					addChild( label );
					
					_fitToBitmap();	
				}
				else
				{
					_aLabels.push( null );			
				}				
				return true;
			}
			else return false;
		}
		
		/**
		 * Removes the passed-in <code>MonitorField</code> from the monitor.
		 * 
		 * <p>Returns <code>true</code> if the field have been removed from
		 * the monitor, <code>false</code> otherwise.</p>
		 * 
		 * @param  	o	<code>MonitorField</code> to remove from the monitor.	
		 * @return 	<code>true</code> if the field have been removed from
		 * 			the monitor, <code>false</code> otherwise.
		 */
		public function removeField ( o : MonitorField ) : Boolean
		{
			if( o == null )
			{
				KairosDebug.WARN( "Attempt to remove a null field from " + this );
				return false;
			}
			else if( !containsField( o ) )
				return false;
			
			var id : Number = _aFields.indexOf( o );
			_aFields.splice( id, 1 );
			
			if( o.isLabel () )
			{
				removeChild( _aLabels.splice( id, 1 ) as MonitorFieldLabel );
				
				_fitToBitmap();
			}
			else
			{
				_aLabels.splice( id, 1 );
			}	
			
			return true;
		}
		
		/**
		 * Returns <code>true</code> if the <code>MonitorField</code>
		 * exist in the monitor.
		 * 
		 * @param  	o	<code>MonitorField</code> to verify if it's in the monitor.	
		 * @return 	<code>true</code> if the field exist in the monitor,
		 * 			<code>false</code> otherwise.
		 */
		public function containsField ( o : MonitorField ) : Boolean
		{
			return (_aFields.indexOf( o ) != -1 );
		}
				
		/**
		 * Process all animations according to received value
		 * from the <code>RTBeacon</code>.
		 * 
		 * @param	e	<code>RTEvent</code> passed by the <code>RTBeacon</code>.
		 */
		public function onTick(e:Event=null):void
		{
			var evt : RTEvent = e as RTEvent;
			var l : Number = _aFields.length;
			
			__move();
			
			while ( --l -(-1) )
			{
				var f : MonitorField = _aFields[ l ] as MonitorField;
				var label : MonitorFieldLabel = _aLabels[ l ] as MonitorFieldLabel;
				
				f.registerValue( evt );
				
				if( f.isLabel() ) label.update();
				if( f.isCurve() && f.isEnabled() ) __drawLine( __nBMPWidth-1, f.getDifference(), f.getColor() );
			}
		}
		/**
		 * Starts the animation.
		 * 
		 * <p>If the object is allready playing the function don't
		 * do anything.</p>
	  	 */
		public function start () : void
		{
			if( !_bIsRunning )
			{
				_bIsRunning = true;
				_oBeacon.addTickListener ( this );
			}
		}
		
		/**
		 * <code>start</code> alias.
		 * 
		 * @see	#start
	  	 */
		public function run () : void
		{
			start();
		}
		
		/**
		 * <code>start</code> alias.
		 * 
		 * @see	#start
	  	 */
		public function play () : void
		{
			start();
		}
		
		/**
		 * Stops the animation.
		 * 
		 * <p>If the object is allready stopped the function don't
		 * do anything.</p>
		 */
		public function stop () : void
		{
			if( _bIsRunning )
			{
				_bIsRunning = false;
				_oBeacon.removeTickListener ( this );
			}
		}
		
		/**
		 * Unsupported method
		 */		
		public function reset () : void
		{
			throw new UnsupportedOperationException ( "The reset method of the Suspendable interface is not supported by Monitor" );
		}
		
		/**
		 * Is the <code>RTObject</code> currently playing ?
		 * 
		 * @return <code>true</code> if the object is currently running,
		 * 		   either <code>false</code>
		 */
		public function isRunning () : Boolean
		{
			return _bIsRunning;
		}
		
		/**
		 * Defines a new <code>TickBeacon</code> object the object will run on.
		 * 
		 * <p>Use that method carefully, if you have objects in your scene which 
		 * are plugged on two beacon which have differents settings you can loose
		 * synchronisation between those objects.</p>
		 * 
		 * <p>If the passed-in beacon is null, the function failed and trace an error
		 * on the Kairos log channel.</p>
		 * 
		 * @param beacon <code>TickBeacon</code> to register with.
		 * @throws	<code>NullPointerException</code> — The passed-in beacon is null.
		 */
		public function setTickBeacon( beacon : RTBeacon ) : void
		{
			if( !beacon ) 
			{
				KairosDebug.WARN( "Trying to set a null beacon as beacon for " + this );
				throw new NullPointerException ( "Trying to set a null beacon as beacon for " + this );
				return; 
			}
			
			if( _bIsRunning )
			{ 
				stop();
				_oBeacon = beacon;
				start();
			}
			else
			{
				_oBeacon = beacon;
			}
		}
		
		/**
		 * @inheritDoc
		 */
		public function getTickBeacon () : RTBeacon
		{
			return _oBeacon as RTBeacon;
		}
		 
		/**
		 * @inheritDoc
		 */
		public function setLocalSpeed ( rate : Number ) : void
		{
			KairosDebug.ERROR( this + " don't support the setLocalSpeed function of the RTAnimation interface" );
			throw new UnsupportedOperationException ( this + " don't support the setLocalSpeed function of the RTAnimation interface" );
		}
		
		/**
		 * @inheritDoc
		 */
		public function getLocalSpeed () : Number
		{
			KairosDebug.ERROR( this + " don't support the getLocalSpeed function of the RTAnimation interface" );
			throw new UnsupportedOperationException ( this + " don't support the getLocalSpeed function of the RTAnimation interface" );
		}
		
		/**
		 * Clears all bitmaps used by the object.
		 */
		public function dispose () : void
		{
			__bmpEmpty.dispose();
			__bmpGraph.dispose();
			__bmpTemp.dispose();
		}
		
		/**
		 * Returns the String representation of the object. 
		 * 
		 * @return	<code>String</code> representation of the object. 
		 */	
		public override function toString() : String
		{
			return PixlibStringifier.stringify( this );
		}
		
		/*---------------------------------------------------------------
		
			PRIVATE API
		
		---------------------------------------------------------------*/
		
		/**
		 * Builds the whole graphics for the monitor. 
		 */
		protected function _createUI() : void
		{			
			__bmpEmpty = new BitmapData ( __nBMPWidth, __nBMPHeight, true, 0x00000000 );
			__bmpTemp  = new BitmapData ( __nBMPWidth, __nBMPHeight, true, 0x00000000 );
			__bmpGraph = new BitmapData ( __nBMPWidth, __nBMPHeight, true, 0x00000000 );		
			
			__mcBackground = __createBackground( COLOR_BACKGROUND );
			__mcBorder = __createBorder( COLOR_GRAPH_BACKGROUND, COLOR_GRAPH_BORDER );
			
			__oGraphHolder = new Bitmap( __bmpGraph, PixelSnapping.ALWAYS, false );	
			
			addChild( __mcBackground );
			addChild( __mcBorder );
			addChild( __oGraphHolder );
		}
		
		/**
		 * Adjusts all graphics object to the size of the graph specified
		 * in the monitor constructor.
		 */
		protected function _fitToBitmap () : void
		{
			var h : Number = 16;
			var l : Number = _aFields.length;
			var y : Number = __nBMPHeight + __MARGIN * 2;
			
			for(var i : Number = 0; i<l; i++ )
			{
				var field : MonitorField = _aFields[ i ] as MonitorField;
				
				if( field.isLabel() )
				{
					var label : MonitorFieldLabel = _aLabels[ i ] as MonitorFieldLabel;
					
					label.resize( __nBMPWidth, i );
					label.y = y;
					label.x = __MARGIN;
							
					y += __LINE_SPACING + h;
				}
			}
			
			__mcBorder.x = __MARGIN;
			__mcBorder.y = __MARGIN;
			__mcBorder.width = __nBMPWidth;
			__mcBorder.height = __nBMPHeight;
			
			__oGraphHolder.x = __MARGIN;
			__oGraphHolder.y = __MARGIN;
			__mcBackground.width = __nBMPWidth + __MARGIN * 2;
			__mcBackground.height = y + __MARGIN;
		}
		
		/**
		 * Realizes the scroll of the graph bitmap
		 */		
		private function __move() : void
		{
			var rc1 : Rectangle = new Rectangle ( 0, 0, __nBMPWidth , __nBMPHeight );
			var rc2 : Rectangle = new Rectangle ( 1, 0, __nBMPWidth - 1, __nBMPHeight );
			var pt1 : Point = new Point ( 0, 0 );
			
			__bmpTemp.copyPixels( __bmpGraph, rc1, pt1 );
			__bmpGraph.copyPixels( __bmpEmpty, rc1, pt1 );
			__bmpGraph.copyPixels( __bmpTemp, rc2, pt1 );
		}
		
		/**
		 * Draws a vertical line at <code>x</code> in <code>r</code>
		 * 
		 * @param 	x	X position where drawing the line.
		 * @param 	r 	Y values covered by the line.
		 * @param 	col	Color of the line.
		 */
		private function __drawLine ( x:Number, r : Range, col : Number ) : void 
		{
			var ys : Number;
			var ye : Number;
			if( r.min > r.max )
			{
				ys = r.max;
				ye = r.min;
			}
			else
			{
				ys = r.min;
				ye = r.max;
			}
			while ( ys++ <= ye )
			{
				__bmpGraph.setPixel32( x, ys, col );
			}						
		}
		
		/**
		 * Creates and returns a new <code>TextField</code> of color <code>col</code>
		 * 
		 * @param	col	Color of the <code>TextField</code>
		 * @return  The new <code>TextField</code>
		 */
		private function __createTextField ( col : Number ) : TextField
		{
			var txt : TextField = new TextField ();
			var format : TextFormat = new TextFormat( "Verdana", 10, col );
			txt.defaultTextFormat = format;
			//txt.autoSize = TextFieldAutoSize.LEFT;
			txt.width = Math.round( __nBMPWidth / 2 );
			txt.height = 16;
			txt.selectable = false;
			txt.text = "----";
			return txt;
		}
		
		/**
		 * Creates and returns a <code>Shape</code> object which represent the border 
		 * of the bitmap graph.
		 * 
		 * @return	A new <code>Shape</code> object. 
		 */
		private function __createBorder( col1 : Number, col2:Number ) : Shape
		{
			var sh : Shape = new Shape();
			var gfx : Graphics = sh.graphics;
			
			gfx.beginFill( col1 );
			gfx.lineStyle( 0, col2 );
			gfx.drawRect( 0, 0, 10, 10 );
			gfx.endFill();

			return sh;	
		}
		/**
		 * Creates and returns a <code>Shape</code> object which represent the
		 * background of the monitor.
		 * 
		 * @return	A new <code>Shape</code> object. 
		 */
		private function __createBackground( col : Number ) : Shape
		{
			var sh : Shape = new Shape();
			var gfx : Graphics = sh.graphics;
			
			gfx.beginFill( col );
			gfx.drawRect( 0, 0, 10, 10 );
			gfx.endFill();
			
			return sh;	
		}
		
		/**
		 * Paints button on passed-in <code>target</code> to represent "Toggle button" for 
		 * graph view.
		 * 
		 * @param target The <code>Sprite</code> paint target.
		 * @param color Color to use to draw the button
		 * @param hole Draw a hole ( <code>true</code> or not <code>false</code> )
		 */
		private function __drawButton( target : Sprite, color : Number, hole : Boolean ) : void 
		{
			target.graphics.clear();
			
			if( hole )
			{
				target.graphics.lineStyle( 1, color );
				target.graphics.beginFill( 0x000000, 0 );
			}
			else target.graphics.beginFill( color );
			
			target.graphics.drawRect( 0, 0, 10, 10 );
		}
		
		/**
		 * Field button callback.
		 * 
		 * <p>Switch button and field state.</p>
		 * 
		 * @param mc <code>Sprite</code> target.
		 * @param field <code>MonitorField<code> target.
		 * @param rest	<code>MouseEvent</code> dispatched with the click event.
		 */
		private function __toggleField( target : Sprite,  field : MonitorField, ... rest ) : void 
		{		
			field.setEnabled( !field.isEnabled() );
			
			__drawButton( target, field.getColor(), !field.isEnabled() );
		}
	}
}

import flash.display.Graphics;
import flash.display.Shape;
import flash.display.Sprite;
import flash.events.MouseEvent;
import flash.text.TextField;
import flash.text.TextFormat;

import com.kairos.utils.Monitor;
import com.kairos.utils.monitor.MonitorField;

/**
 * <code>MonitorFieldLabel</code> is used by the <code>Monitor</code> internally to render
 * the labels field.</code>
 * 
 * <p>A <code>MonitorFieldLabel</code> offer a toggle button to switch the display state of
 * the field's curve within the graph, if the field doesn't display any curve the toggle
 * button is omitted.</p>
 */
internal class MonitorFieldLabel extends Sprite
{
	private static const __MARGIN : Number = 5;
	
	protected var _oField : MonitorField;
	protected var _oButton : Sprite;
	protected var _oLabel : TextField;
	protected var _oValue : TextField;
	protected var _oBackground : Shape;
	
	/**
	 * Creates a new <code>MonitorFieldLabel</code> object targeted
	 * on the passed-in <code>MonitorField</code>.
	 * 
	 * @param field The <code>MonitorField</code> to display as label.
	 */	
	public function MonitorFieldLabel ( field : MonitorField )
	{
		_oField =  field;

		_oBackground = __createBackground( Monitor.COLOR_BACKGROUND );	
		
		addChild( _oBackground	);

		if( _oField.isCurve() )
		{
			_oButton = new Sprite();
			_oButton.buttonMode = true;
			_oButton.useHandCursor = false;
			_oButton.addEventListener( MouseEvent.CLICK, __toggleField );
			__drawButton( _oButton, _oField.getColor(), !_oField.isEnabled() );
			
			addChild( _oButton );
		}
		
		_oLabel = __createTextField( _oField.getColor() );
		_oValue = __createTextField( Monitor.COLOR_VALUE );
		
		addChild( _oLabel );
		addChild( _oValue );	
	}
	
	
	/**
	 * Updates the size of the label according to the size of the graph in the monitor.
	 * 
	 * <p>The second parameter indicates the position of the label in the label's list
	 * in order to alternates background color of the field.</p>
	 * 
	 * @param w <code>Number</code> size of the graph in pixels
	 * @param p	<code>Number</code> position of the label within the monitor
	 */
	public function resize ( w : Number, p : Number ) : void
	{
		if( _oButton )
		{
			_oButton.x = __MARGIN - 3;
			_oButton.y = 3;
		}
		
		_oLabel.width = w / 2 - 10;
		_oLabel.x = __MARGIN + 10;
		_oLabel.text = _oField.getName();
		
		_oValue.width = w / 2 - __MARGIN*2;
		_oValue.x = __MARGIN * 2 + w / 2;
		
		__fillBackground ( p % 2 == 0 ? Monitor.COLOR_BACKGROUND_ALTERNATE1 : Monitor.COLOR_BACKGROUND_ALTERNATE2 );
		_oBackground.width = w;
		_oBackground.height = 16;
	}
	
	/**
	 * Updates the label with data extracted from the <code>MonitorField</code> target. 
	 */
	public function update () : void
	{
		_oValue.text = _oField.getFormattedValue();
	}

	private function __createTextField ( col : Number ) : TextField
	{
		var txt : TextField = new TextField ();
		var format : TextFormat = new TextFormat( "Verdana", 10, col );
		txt.defaultTextFormat = format;
		txt.height = 16;
		txt.selectable = false;
		txt.text = "----";
		return txt;
	}
	
	private function __createBackground( col : Number ) : Shape
	{
		var sh : Shape = new Shape();
		var gfx : Graphics = sh.graphics;
		
		gfx.beginFill( col );
		gfx.drawRect( 0, 0, 10, 10 );
		gfx.endFill();
		
		return sh;	
	}
	
	private function __fillBackground ( col : Number ):  void
	{
		var gfx : Graphics = _oBackground.graphics;
		
		gfx.clear();
		gfx.beginFill( col );
		gfx.drawRect( 0, 0, 10, 10 );
		gfx.endFill();
	}

	private function __drawButton( target : Sprite, color : Number, hole : Boolean ) : void 
	{
		target.graphics.clear();
		
		if( hole )
		{
			target.graphics.lineStyle( 1, color );
			target.graphics.beginFill( 0x000000, 0 );
		}
		else target.graphics.beginFill( color );
		
		target.graphics.drawRect( 0, 0, 10, 10 );
	}
	
	private function __toggleField( ... rest ) : void 
	{		
		_oField.setEnabled( !_oField.isEnabled() );
		
		__drawButton( _oButton, _oField.getColor(), !_oField.isEnabled() );
	}
}